Differenct Layer, Different Abstruction
「異なるレイヤでは、異なる抽象化を持つべき」
アプリケーションはレイヤで構成される
上位レイヤは下位レイヤが提供する機能を用いる
良い設計のシステムでは、各レイヤはその上下のレイヤとは異なる抽象化を提供する
つまり、ある操作がメソッドを呼び出すことでレイヤを上下に移動するのを追跡すると、メソッドを呼び出すたびに抽象化が変わる
同じような抽象度を持つレイヤが隣接している場合、クラスの分離に問題があるサイン
隣接するレイヤが似たような抽象化を提供している場合、Pass-through methods という形で現れる
Pass-through variables という形でも現れる(Differenct Layer, Different Abstruction#6583c61075d04f0000901f56)
Pass-through methods: 呼び出し元のメソッドとシグネチャが似ているか、別のメソッドを呼び出す以外殆ど何もしないメソッド
単に委譲しているだけのメソッドなどradish-miyazaki.icon
「何もしない」というのが肝 radish-miyazaki.icon
レッドフラグ
Pass-through methods は Shallow Class を促す
インタフェースの複雑さは増す一方、機能が増えることは無い
Pass-through methods は クラス間の責務の分離がなされていないことを示している
解決策: 単一責任の原則 に沿うようにクラスをリファクタリングする
https://scrapbox.io/files/6583a7f06f93490023a35cc9.png
(a): Pass-through methods
(b) 解決策①: 下位クラスを上位クラスの呼び出し元に直接公開する
(c) 解決策②: クラス間で機能を再分配する
(d) 解決策③: クラスを分離できない場合、クラスをマージする
同じシグネチャを持つメソッドが悪いとは限らない
重要なのは、それぞれのメソッドが機能に貢献するかどうか
Pass-through methods は機能に全く貢献していないので悪である
e.g. ディスパッチ
ディスパッチのシグネチャは、呼び出すメソッドのシグネチャと同じになることが多い
しかし、「いくつかのメソッドのうち、どのメソッドを呼び出すべきかを選択する」という機能を持っているので、Pass-through methods ではない
e.g. 複数の実装を持つインタフェース(OS のディスクドライバ)
各ドライバは異なる種類のディスクに対応しているが、インタフェースは同じ
同じインタフェースで異なる実装を提供する複数のメソッドがあると、認知的負荷が軽減する
1つのメソッドを使ったことがあれば、他のメソッドを使うのも簡単
Decorator pattern も Shallow Class を促す
Decorator pattern は基底クラスのオブジェクトを受け取り、その機能を拡張するデザインパターン
デコレータオブジェクトは、基底オブジェクトと同一または類似したインタフェースを提供する
そのメソッドは基底オブジェクトのメソッドを呼び出す
Pass-through methods -> Shallow Class
ちょっとした機能を追加するだけなのに、大量の定型文を書く必要がある
Decorator pattern を使いすぎると、小さな機能ごとに新しいクラスを作ることになりがち
Shallow Class が増える
e.g. Java I/O
code:java
fileInputStream fileStream = new FileInputStream(fileName);
BufferedInputStream bufferedStream = new BufferedInputStream(fileStream);
ObjectInputStream objectStream = new ObjectInputStream(bufferedStream);
Modules Should Be Deep#65813ac475d04f000030ab5b
新しいデコレータオブジェクトを作成する前に、以下のような代替案を検討するのが良い
基底オブジェクトに新しい機能を追加することはできないか?
以下のような条件を満たす場合に有用
機能が「ある程度」汎用的
基底オブジェクトと論理的に関連している
基底クラスの大部分でで新しい機能が使用される
e.g. Java I/O
InputStream を作成する人の殆どは BufferedInputStream も作成するので、これらのクラスは統合したほうが良い
新しい機能があるユースケースに特化している場合、それをユースケースと統合したほうが理にかなっていないか?
既存のデコレータに新しい機能を追加できないか?
基底オブジェクトとは別のクラスとして実装できないか?
クラスのインタフェースと実装は、異なる抽象度を持つべき
似たような抽象度を持っている場合、そのクラスはあまり深くない(Deep Class)
e.g. テキストエディタ(テキスト操作に関するモジュール)
インタフェースが行単位
高レベルの UI レイヤでは、行の途中にテキストを挿入したり、範囲選択したテキストを削除したりする必要があるので、行を分割したり結合したりする必要がある
インタフェースが文字単位
行の分割や結合の複雑さをモジュール内でカプセル化し、Deep Module にすることができる
これにより、UI レイヤのコードもシンプルになる
Pass-through methods の他にも、Pass-through variables という形で現れる。
Pass-through variables: 長い一連のメソッドを通じて受け渡される変数のこと
props drilling をイメージすると分かりやすいradish-miyazaki.icon
https://scrapbox.io/files/6583c6e86fc13f0023c66089.png
Pass-through variables は、すべての中間メソッドにその存在を認識させるので、複雑さを増加させる
場合によっては、新しい変数を追加する場合、関連するすべてのインタフェースとメソッドを変更する必要がある
解決策:
(b): 最上位と最下位のメソッド間で共有されている既存のオブジェクトが存在しないかの確認する
そもそもこのオブジェクトが Pass-through variables なのかも?
(c): グローバル変数に格納する
グローバル変数のアクセスがコンフリクトする可能性がある
(d): コンテキストオブジェクトの導入
筆者のおすすめ
コンテキスト: アプリケーションのすべてのグローバルステート(Pass-through variables や グローバス変数)を格納する
e.g. context.Context radish-miyazaki.icon
ただし、コンテキストオブジェクト自体が Pass-through variables になる可能性もある
無限ループってこわくね?
回避策
コンテキストへの参照はシステムの大部分を占める主要なオブジェクト内にインスタンス変数として保存しておくのが良い
e.g. (d) : m3 を含むクラスは、オブジェクトのインスタンス変数にコンテキストへの参照を格納する
これにより、コンテキストオブジェクトはどこからでも使えるが、顕在化するのはコンストラクタの明示的な引数としてのみ
テストもしやすくなる
コンテキストオブジェクトの変数をテスト用の値と差し替えるだけで良いradish-miyazaki.icon
デメリットを抑えるためにも、コンテキスト内の変数はイミュータブルなほうが好ましい
デメリット
多くは、グローバル変数で起こるデメリットと近しい
ある変数がなぜ存在するのか、どこで使われているのかが明らかでない
ルールがないとコンテキストオブジェクトが膨大になり、不明瞭な依存関係を生み出す可能性になる
スレッドセーフの問題を引き起こす可能性も
hr.icon
要約
インタフェースや引数、関数、クラス、定義といったシステムを構成する各要素は、開発者がこれらの要素について学習しなければならないので、複雑さを増加させる
その複雑さに対する費用対効果を最大限得るためには、「その要素がない状態」で発生する複雑さを取り除く必要がある
取り除けないならば、その要素を追加することなく実装したほうが良い
複雑さを取り除く例: クラスによるカプセル化
「異なるレイヤでは、異なる抽象化を持つべき」というルールは、上記の考え方の応用に過ぎない
異なるレイヤが同じ抽象化をもつ場合(e.g. Pass-through methods / Decorator pattern)、複雑さを増加させたことによる費用対効果を十分に得られない可能性が高い
複雑さ(費用)が著しく高い radish-miyazaki.icon
Pass-through variables も同様で、存在を認識するためにいくつかのメソッドが必要であり、これが複雑さを生んでいる
その一方で、追加の機能(効果)は全く得られない
#読書メモ